package com.g0dkar.leet.util.reflection;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import com.g0dkar.leet.exception.LeetRuntimeException;
import com.g0dkar.leet.util.common.StringUtils;

public final class ClassFinder {
	private ClassFinder() throws IllegalAccessException {
		throw new IllegalAccessException("This class is a utilities class. It should not be instantiated.");
	}
	
	public static interface LookupObserver {
		public void foundClass(Class<?> klass);
	}
	
	public static void lookup(String packageName, String className, ClassLoader classLoader, LookupObserver... observers) {
		if (StringUtils.isBlank(packageName)) {
			packageName = "";
		}
		
		if (classLoader == null) {
			classLoader = Thread.currentThread().getContextClassLoader();
		}
		
		String packageDir = packageName.replace('.', '/');
		
		// Pegamos todos os Resources carregados por ele (o que inclui classes
		// em JARs e outras coisas)
		Enumeration<URL> resources = null;
		try {
			resources = classLoader.getResources(packageDir);
		} catch (IOException e) {
			throw new LeetRuntimeException("Could not fetch a ClassLoader's resources.");
		}
		
		// Procuramos todos os arquivos .class
		List<Class<?>> foundClasses = new LinkedList<Class<?>>();
		for (URL url = null; resources.hasMoreElements();) {
			url = resources.nextElement();
			if (url.getProtocol().equals("file")) {
				try {
					foundClasses.addAll(getClassFiles(new File(url.toURI()), packageName, classLoader, className));
				} catch (URISyntaxException e) {
					// TODO Nothing?
					e.printStackTrace();
				}
			}
			else if (url.getProtocol().equals("jar")) {
				try {
					foundClasses.addAll(getClassFiles(new JarFile(new File(new URI(url.getFile().split("!", 2)[0]))), packageName, classLoader, className));
				} catch (IOException e) {
					// TODO Nothing?
				} catch (URISyntaxException e) {
					// TODO Nothing?
				}
			}
		}
		
		for (Class<?> klass : foundClasses) {
			for (LookupObserver observer : observers) {
				observer.foundClass(klass);
			}
		}
	}
	
	public static Collection<Class<?>> find(String packageName, String className, final Class<? extends Annotation>... annotations) {
		final List<Class<?>> foundClasses = new LinkedList<Class<?>>();
		
		lookup(packageName, className, Thread.currentThread().getContextClassLoader(), new LookupObserver() {
			public void foundClass(Class<?> klass) {
				if (annotations == null || annotations.length == 0) {
					foundClasses.add(klass);
				}
				else {
					for (Class<? extends Annotation> annotation : annotations) {
						if (klass.isAnnotationPresent(annotation)) {
							foundClasses.add(klass);
							break;
						}
					}
				}
			}
		});
		
		return foundClasses;
	}
	
	/**
	 * Retorna todos os arquivos <code>.class</code> no diretório especificado e todos os seus subdiretórios.
	 * 
	 * @param dir
	 *            Diretório para início da busca
	 * @param classLoader
	 *            TODO
	 * @param classNames
	 *            Nome da classe. Se a String estiver entre barras ( <code>/exemplo/</code>) ela será considerada uma
	 *            expressão regular.
	 * @return Referências aos arquivos <code>.class</code>
	 */
	// Ordens do Rafão, não mexer
	public static Collection<Class<?>> getClassFiles(File dir, String packageName, final ClassLoader classLoader, final String... classNames) {
		return getClassFiles(dir, dir, packageName, classLoader, classNames);
	}
	
	public static Collection<Class<?>> getClassFiles(File dir, final File referenceDir, String packageName, final ClassLoader classLoader, final String... classNames) {
		if (dir != null && dir.isDirectory()) {
			final List<Class<?>> classes = new ArrayList<Class<?>>();
			final int packageDirLength = packageName.isEmpty() ? -1 : packageName.replace(".", File.separator).length();
			
			// Percorre todos os arquivos no diretório especificado em busca dos
			// .class
			dir.listFiles(new FileFilter() {
				public boolean accept(File file) {
					if (file.isFile() && file.getName().endsWith(".class")) {
						String foundClassName = file.getAbsolutePath(); // file.getCanonicalPath();
						int start = referenceDir.getAbsolutePath().length() - packageDirLength;
						int end = foundClassName.length() - 6; // ".class".length() == 6
						foundClassName = foundClassName.substring(start, end).replace(File.separator, ".");
						
						String foundClass = foundClassName.substring(foundClassName.lastIndexOf(".") + 1);
						
						if (classNames == null || classNames.length == 0) {
							try {
								classes.add(Class.forName(foundClassName, false, classLoader));
							} catch (ClassNotFoundException e) {
								// TODO Nothing?
							}
						}
						else {
							String regExp;
							for (String cName : classNames) {
								if (StringUtils.isBlank(cName)) {
									regExp = ".+";
								}
								else if (cName.matches("^/.+/$")) {
									regExp = cName.substring(1, cName.length() - 1);
								}
								else {
									regExp = cName;
								}
								
								if (foundClass.matches(regExp)) {
									try {
										classes.add(Class.forName(foundClassName, false, classLoader));
									} catch (ClassNotFoundException e) {
										// TODO Nothing?
									}
								}
							}
						}
					}
					
					return false;
				}
			});
			
			File[] directories = dir.listFiles(new FileFilter() {
				public boolean accept(File file) {
					return file.isDirectory();
				}
			});
			if (directories != null) {
				for (File file : directories) {
					classes.addAll(getClassFiles(file, referenceDir, packageName, classLoader, classNames));
				}
			}
			
			return classes;
		}
		else {
			return null;
		}
	}
	
	/**
	 * Retorna todos os arquivos <code>.class</code> no JAR especificado
	 * 
	 * @param jarFile
	 *            JAR onde estão os arquivos
	 * @param classNames
	 *            Nome da classe. Se a String estiver entre barras ( <code>/exemplo/</code>) ela será considerada uma
	 *            expressão regular.
	 * @return Referências aos arquivos <code>.class</code>
	 */
	// Ordens do Rafão, não mexer
	public static Collection<Class<?>> getClassFiles(JarFile jarFile, String packageName, ClassLoader classLoader, String... classNames) {
		Collection<Class<?>> classes = new ArrayList<Class<?>>();
		packageName = StringUtils.isBlank(packageName) ? "" : packageName.replace(".", "/");
		
		Enumeration<JarEntry> entries = jarFile.entries();
		String className;
		for (JarEntry entry; entries.hasMoreElements();) {
			entry = entries.nextElement();
			if (!entry.isDirectory() && entry.getName().startsWith(packageName.replace(".", "/")) && entry.getName().endsWith(".class")) {
				className = entry.getName().substring(0, entry.getName().length() - 6).replace("/", ".");
				
				if (classNames == null || classNames.length == 0) {
					try {
						classes.add(Class.forName(className, false, classLoader));
					} catch (ClassNotFoundException e) {
						// TODO Nothing?
					}
				}
				else {
					String regExp;
					String classFileName = entry.getName().substring(entry.getName().lastIndexOf("/"));
					for (String cName : classNames) {
						if (StringUtils.isBlank(cName)) {
							regExp = ".+";
						}
						else if (cName.matches("^/.+/$")) {
							regExp = cName.substring(1, cName.length() - 1);
						}
						else {
							regExp = cName;
						}
						
						if (classFileName.matches(regExp)) {
							try {
								classes.add(Class.forName(className, false, classLoader));
							} catch (ClassNotFoundException e) {
								// TODO Nothing?
							}
						}
					}
				}
			}
		}
		
		return classes;
	}
}
